/**
 * Copyright 2005-2010 Noelios Technologies.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL 1.0 (the
 * "Licenses"). You can select the license that you prefer but you may not use
 * this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0.html
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1.php
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1.php
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.noelios.com/products/restlet-engine
 * 
 * Restlet is a registered trademark of Noelios Technologies.
 */

package org.restlet.data;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

import org.restlet.Context;
import org.restlet.engine.Edition;

/**
 * Reference to a Uniform Resource Identifier (URI). Contrary to the
 * java.net.URI class, this interface represents mutable references. It strictly
 * conforms to the RFC 3986 specifying URIs and follow its naming conventions.<br>
 * 
 * <pre>
 * URI reference        = absolute-reference | relative-reference
 * 
 * absolute-reference   = scheme &quot;:&quot; scheme-specific-part [ &quot;#&quot; fragment ]
 * scheme-specific-part = ( hierarchical-part [ &quot;?&quot; query ] ) | opaque-part
 * hierarchical-part    = ( &quot;//&quot; authority path-abempty ) | path-absolute | path-rootless | path-empty
 * authority            = [ user-info &quot;@&quot; ] host-domain [ &quot;:&quot; host-port ]
 * 
 * relative-reference   = relative-part [ &quot;?&quot; query ] [ &quot;#&quot; fragment ]
 * relative-part        = ( &quot;//&quot; authority path-abempty ) | path-absolute | path-noscheme | path-empty
 * 
 * path-abempty         = begins with &quot;/&quot; or is empty
 * path-absolute        = begins with &quot;/&quot; but not &quot;//&quot;
 * path-noscheme        = begins with a non-colon segment
 * path-rootless        = begins with a segment
 * path-empty           = zero characters
 * </pre>
 * 
 * <p>
 * Note that this class doesn't encode or decode the reserved characters. It
 * assumes that the URIs or the URI parts passed in are properly encoded using
 * the standard URI encoding mechanism. You can use the static "encode()" and
 * "decode()" methods for this purpose. Note that if an invalid URI character is
 * detected by the constructor or one of the setters, a trace will be logged and
 * the character will be automatically encoded.
 * </p>
 * <p>
 * The fundamental point to underline is the difference between an URI
 * "reference" and an URI. Contrary to an URI (the target identifier of a REST
 * resource), an URI reference can be relative (with or without query and
 * fragment part). This relative URI reference can then be resolved against a
 * base reference via the getTargetRef() method which will return a new resolved
 * Reference instance, an absolute URI reference with no base reference and with
 * no dot-segments (the path segments "." and "..").
 * </p>
 * <p>
 * You can also apply the getTargetRef() method on absolute references in order
 * to solve the dot-segments. Note that applying the getRelativeRef() method on
 * an absolute reference returns the current reference relatively to a base
 * reference, if any, and solves the dot-segments.
 * </p>
 * <p>
 * The Reference stores its data as a single string, the one passed to the
 * constructor. This string can always be obtained using the toString() method.
 * A couple of integer indexes are maintained to improve the extraction time of
 * various reference properties (URI components).
 * </p>
 * <p>
 * When you modify a specific component of the URI reference, via the setPath()
 * method for example, the internal string is simply regenerated by updating
 * only the relevant part. We try as much as possible to protect the bytes given
 * to the Reference class instead of transparently parsing and normalizing the
 * URI data. Our idea is to protect encodings and special characters in all case
 * and reduce the memory size taken by this class while making Reference
 * instances mutable.
 * </p>
 * <p>
 * Because the base reference is only a property of the Reference ("baseRef").
 * When you use the "Reference(base, path)" constructor, it is equivalent to
 * doing:<br>
 * ref = new Reference(path);<br>
 * ref.setBaseRef(base);
 * </p>
 * <p>
 * The base ref is not automatically resolved or "merged" with the rest of the
 * reference information (the path here). For example, this let's you reuse a
 * single reference as the base of several relative references. If you modify
 * the base reference, all relative references are still accurate.
 * </p>
 * Note that the name and value properties are thread safe, stored in volatile
 * members.
 * 
 * @author Jerome Louvel
 * @see <a href="http://tools.ietf.org/html/rfc3986">RFC 3986</a>
 */
public class Reference {

    /** Helps to map characters and their validity as URI characters. */
    private static final boolean[] charValidityMap = new boolean[127];

    static {
        // Initialize the map af valid characters.
        for (int character = 0; character < 127; character++) {
            charValidityMap[character] = isReserved(character) || isUnreserved(character)
                    || (character == '%');
        }
    }

    /**
     * Decodes a given string using the standard URI encoding mechanism and the
     * UTF-8 character set.
     * 
     * @param toDecode
     *            The string to decode.
     * @return The decoded string.
     */
    public static String decode(String toDecode) {
        String result = null;

        if (toDecode != null) {
            try {
                result = java.net.URLDecoder.decode(toDecode, "UTF-8");
            } catch (UnsupportedEncodingException uee) {
                Context
                        .getCurrentLogger()
                        .log(
                                Level.WARNING,
                                "Unable to decode the string with the UTF-8 character set.",
                                uee);
            }


        }

        return result;
    }

    /**
     * Decodes a given string using the standard URI encoding mechanism. If the
     * provided character set is null, the string is returned but not decoded.
     * <em><strong>Note:</strong> The <a
     * href="http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars">
     * World Wide Web Consortium Recommendation</a> states that UTF-8 should be
     * used. Not doing so may introduce incompatibilities.</em>
     * 
     * @param toDecode
     *            The string to decode.
     * @param characterSet
     *            The name of a supported character encoding.
     * @return The decoded string or null if the named character encoding is not
     *         supported.
     */
    public static String decode(String toDecode, CharacterSet characterSet) {
        if (Edition.CURRENT == Edition.GWT) {
            if (!CharacterSet.UTF_8.equals(characterSet)) {
                throw new IllegalArgumentException(
                        "Only UTF-8 URL encoding is supported under GWT");
            }
        }
        String result = null;
        try {
            result = (characterSet == null) ? toDecode : java.net.URLDecoder
                    .decode(toDecode, characterSet.getName());
        } catch (UnsupportedEncodingException uee) {
            Context
                    .getCurrentLogger()
                    .log(
                            Level.WARNING,
                            "Unable to decode the string with the UTF-8 character set.",
                            uee);
        }


        return result;
    }

    /**
     * Encodes a given string using the standard URI encoding mechanism and the
     * UTF-8 character set.
     * 
     * @param toEncode
     *            The string to encode.
     * @return The encoded string.
     */
    public static String encode(String toEncode) {
        String result = null;

        if (toEncode != null) {
            try {
                result = java.net.URLEncoder.encode(toEncode, "UTF-8");
            } catch (UnsupportedEncodingException uee) {
                Context
                        .getCurrentLogger()
                        .log(
                                Level.WARNING,
                                "Unable to encode the string with the UTF-8 character set.",
                                uee);
            }


        }

        return result;
    }

    /**
     * Encodes a given string using the standard URI encoding mechanism. If the
     * provided character set is null, the string is returned but not encoded.
     * 
     * <em><strong>Note:</strong> The <a
     * href="http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars">
     * World Wide Web Consortium Recommendation</a> states that UTF-8 should be
     * used. Not doing so may introduce incompatibilites.</em>
     * 
     * @param toEncode
     *            The string to encode.
     * @param characterSet
     *            The supported character encoding.
     * @return The encoded string or null if the named character encoding is not
     *         supported.
     */
    public static String encode(String toEncode, CharacterSet characterSet) {
        if (Edition.CURRENT == Edition.GWT) {
            if (!CharacterSet.UTF_8.equals(characterSet)) {
                throw new IllegalArgumentException(
                        "Only UTF-8 URL encoding is supported under GWT");
            }
        }

        String result = null;

        try {
            result = (characterSet == null) ? toEncode : java.net.URLEncoder
                    .encode(toEncode, characterSet.getName());
        } catch (UnsupportedEncodingException uee) {
            Context
                    .getCurrentLogger()
                    .log(
                            Level.WARNING,
                            "Unable to encode the string with the UTF-8 character set.",
                            uee);
        }


        return result;
    }

    /**
     * Indicates if the given character is alphabetical (a-z or A-Z).
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is alphabetical (a-z or A-Z).
     */
    private static boolean isAlpha(int character) {
        return isUpperCase(character) || isLowerCase(character);
    }

    /**
     * Indicates if the given character is a digit (0-9).
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is a digit (0-9).
     */
    private static boolean isDigit(int character) {
        return (character >= '0') && (character <= '9');
    }

    /**
     * Indicates if the given character is a generic URI component delimiter
     * character.
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is a generic URI component delimiter
     *         character.
     */
    public static boolean isGenericDelimiter(int character) {
        return (character == ':') || (character == '/') || (character == '?')
                || (character == '#') || (character == '[')
                || (character == ']') || (character == '@');
    }

    /**
     * Indicates if the given character is lower case (a-z).
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is lower case (a-z).
     */
    private static boolean isLowerCase(int character) {
        return (character >= 'a') && (character <= 'z');
    }

    /**
     * Indicates if the given character is a reserved URI character.
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is a reserved URI character.
     */
    public static boolean isReserved(int character) {
        return isGenericDelimiter(character) || isSubDelimiter(character);
    }

    /**
     * Indicates if the given character is an URI subcomponent delimiter
     * character.
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is an URI subcomponent delimiter
     *         character.
     */
    public static boolean isSubDelimiter(int character) {
        return (character == '!') || (character == '$') || (character == '&')
                || (character == '\'') || (character == '(')
                || (character == ')') || (character == '*')
                || (character == '+') || (character == ',')
                || (character == ';') || (character == '=');
    }

    /**
     * Indicates if the given character is an unreserved URI character.
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is an unreserved URI character.
     */
    public static boolean isUnreserved(int character) {
        return isAlpha(character) || isDigit(character) || (character == '-')
                || (character == '.') || (character == '_')
                || (character == '~');
    }

    /**
     * Indicates if the given character is upper case (A-Z).
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is upper case (A-Z).
     */
    private static boolean isUpperCase(int character) {
        return (character >= 'A') && (character <= 'Z');
    }

    /**
     * Indicates if the given character is a valid URI character.
     * 
     * @param character
     *            The character to test.
     * @return True if the given character is a valid URI character.
     */
    public static boolean isValid(int character) {
        return character >= 0 && character < 127 && charValidityMap[character];
    }

    /**
     * Creates a reference string from its parts.
     * 
     * @param scheme
     *            The scheme ("http", "https" or "ftp").
     * @param hostName
     *            The host name or IP address.
     * @param hostPort
     *            The host port (default ports are correctly ignored).
     * @param path
     *            The path component for hierarchical identifiers.
     * @param query
     *            The optional query component for hierarchical identifiers.
     * @param fragment
     *            The optional fragment identifier.
     * @return The reference as String.
     */
    public static String toString(String scheme, String hostName,
            Integer hostPort, String path, String query, String fragment) {
        String host = hostName;

        // Appends the host port number
        if (hostPort != null) {
            final int defaultPort = Protocol.valueOf(scheme).getDefaultPort();
            if (hostPort != defaultPort) {
                host = hostName + ':' + hostPort;
            }
        }

        return toString(scheme, host, path, query, fragment);
    }

    /**
     * Creates a relative reference string from its parts.
     * 
     * @param relativePart
     *            The relative part component.
     * @param query
     *            The optional query component for hierarchical identifiers.
     * @param fragment
     *            The optional fragment identifier.
     * @return The relative reference as a String.
     */
    public static String toString(String relativePart, String query,
            String fragment) {
        final StringBuilder sb = new StringBuilder();

        // Append the path
        if (relativePart != null) {
            sb.append(relativePart);
        }

        // Append the query string
        if (query != null) {
            sb.append('?').append(query);
        }

        // Append the fragment identifier
        if (fragment != null) {
            sb.append('#').append(fragment);
        }

        // Actually construct the reference
        return sb.toString();
    }

    /**
     * Creates a reference string from its parts.
     * 
     * @param scheme
     *            The scheme ("http", "https" or "ftp").
     * @param host
     *            The host name or IP address plus the optional port number.
     * @param path
     *            The path component for hierarchical identifiers.
     * @param query
     *            The optional query component for hierarchical identifiers.
     * @param fragment
     *            The optional fragment identifier.
     * @return The reference a String.
     */
    public static String toString(String scheme, String host, String path,
            String query, String fragment) {
        final StringBuilder sb = new StringBuilder();

        if (scheme != null) {
            // Append the scheme and host name
            sb.append(scheme.toLowerCase()).append("://").append(host);
        }

        // Append the path
        if (path != null) {
            sb.append(path);
        }

        // Append the query string
        if (query != null) {
            sb.append('?').append(query);
        }

        // Append the fragment identifier
        if (fragment != null) {
            sb.append('#').append(fragment);
        }

        // Actually construct the reference
        return sb.toString();
    }

    /** The base reference for relative references. */
    private volatile Reference baseRef;

    /** The fragment separator index. */
    private volatile int fragmentIndex;

    /** The internal reference. */
    private volatile String internalRef;

    /** The query separator index. */
    private volatile int queryIndex;

    /** The scheme separator index. */
    private volatile int schemeIndex;

    /**
     * Empty constructor.
     */
    public Reference() {
        this((Reference) null, (String) null);
    }

    /**
     * Constructor from an {@link java.net.URI} instance.
     * 
     * @param uri
     *            The {@link java.net.URI} instance.
     */
    public Reference(java.net.URI uri) {
        this(uri.toString());
    }

    /**
     * Constructor from an {@link java.net.URL} instance.
     * 
     * @param url
     *            The {@link java.net.URL} instance.
     */
    public Reference(java.net.URL url) {
        this(url.toString());
    }

    /**
     * Constructor for a protocol and host name. Uses the default port for the
     * given protocol.
     * 
     * @param protocol
     *            Protocol/scheme to use
     * @param hostName
     *            The host name or IP address.
     */
    public Reference(Protocol protocol, String hostName) {
        this(protocol, hostName, protocol.getDefaultPort());
    }

    /**
     * Constructor for a protocol, host name and host port
     * 
     * @param protocol
     *            Protocol/scheme to use
     * @param hostName
     *            The host name or IP address.
     * @param hostPort
     *            The host port (default ports are correctly ignored).
     */
    public Reference(Protocol protocol, String hostName, int hostPort) {
        this(protocol.getSchemeName(), hostName, hostPort, null, null, null);
    }

    /**
     * Clone constructor.
     * 
     * @param ref
     *            The reference to clone.
     */
    public Reference(Reference ref) {
        this(ref.baseRef, ref.internalRef);
    }

    /**
     * Constructor from an URI reference (most likely relative).
     * 
     * @param baseRef
     *            The base reference.
     * @param uriReference
     *            The URI reference, either absolute or relative.
     */
    public Reference(Reference baseRef, Reference uriReference) {
        this(baseRef, uriReference.toString());
    }

    /**
     * Constructor from an URI reference (most likely relative).
     * 
     * @param baseRef
     *            The base reference.
     * @param uriRef
     *            The URI reference, either absolute or relative.
     */
    public Reference(Reference baseRef, String uriRef) {
        uriRef = encodeInvalidCharacters(uriRef);
        this.baseRef = baseRef;
        this.internalRef = uriRef;
        updateIndexes();
    }

    /**
     * Constructor of relative reference from its parts.
     * 
     * @param baseRef
     *            The base reference.
     * @param relativePart
     *            The relative part component (most of the time it is the path
     *            component).
     * @param query
     *            The optional query component for hierarchical identifiers.
     * @param fragment
     *            The optional fragment identifier.
     */
    public Reference(Reference baseRef, String relativePart, String query,
            String fragment) {
        this(baseRef, toString(relativePart, query, fragment));
    }

    /**
     * Constructor from an URI reference.
     * 
     * @param uriReference
     *            The URI reference, either absolute or relative.
     */
    public Reference(String uriReference) {
        this((Reference) null, uriReference);
    }

    /**
     * Constructor from an identifier and a fragment.
     * 
     * @param identifier
     *            The resource identifier.
     * @param fragment
     *            The fragment identifier.
     */
    public Reference(String identifier, String fragment) {
        this((fragment == null) ? identifier : identifier + '#' + fragment);
    }

    /**
     * Constructor of absolute reference from its parts.
     * 
     * @param scheme
     *            The scheme ("http", "https" or "ftp").
     * @param hostName
     *            The host name or IP address.
     * @param hostPort
     *            The host port (default ports are correctly ignored).
     * @param path
     *            The path component for hierarchical identifiers.
     * @param query
     *            The optional query component for hierarchical identifiers.
     * @param fragment
     *            The optional fragment identifier.
     */
    public Reference(String scheme, String hostName, int hostPort, String path,
            String query, String fragment) {
        this(toString(scheme, hostName, hostPort, path, query, fragment));
    }

    /**
     * Adds a parameter to the query component. The name and value are
     * automatically encoded if necessary.
     * 
     * @param name
     *            The parameter name.
     * @param value
     *            The optional parameter value.
     * @return The updated reference.
     */
    public Reference addQueryParameter(String name, String value) {
        final String query = getQuery();

        if (query == null) {
            if (value == null) {
                setQuery(encode(name));
            } else {
                setQuery(encode(name) + '=' + encode(value));
            }
        } else {
            if (value == null) {
                setQuery(query + '&' + encode(name));
            } else {
                setQuery(query + '&' + encode(name) + '=' + encode(value));
            }
        }

        return this;
    }

    /**
     * Adds a segment at the end of the path. If the current path doesn't end
     * with a slash character, one is inserted before the new segment value. The
     * value is automatically encoded if necessary.
     * 
     * @param value
     *            The segment value to add.
     * @return The updated reference.
     */
    public Reference addSegment(String value) {
        final String path = getPath();

        if (value != null) {
            if (path == null) {
                setPath("/" + value);
            } else {
                if (path.endsWith("/")) {
                    setPath(path + encode(value));
                } else {
                    setPath(path + "/" + encode(value));
                }
            }
        }

        return this;
    }

    @Override
    public Reference clone() {
        final Reference newRef = new Reference();

        if (this.baseRef == null) {
            newRef.baseRef = null;
        } else if (equals(this.baseRef)) {
            newRef.baseRef = newRef;
        } else {
            newRef.baseRef = this.baseRef.clone();
        }

        newRef.fragmentIndex = this.fragmentIndex;
        newRef.internalRef = this.internalRef;
        newRef.queryIndex = this.queryIndex;
        newRef.schemeIndex = this.schemeIndex;
        return newRef;
    }

    /**
     * Checks if all characters are valid and encodes invalid characters if
     * necessary.
     * 
     * @param uriRef
     *            The URI reference to check.
     * @return The original reference, eventually with invalid URI characters
     *         encoded.
     */
    private String encodeInvalidCharacters(String uriRef)
            throws IllegalArgumentException {
        String result = uriRef;

        if (uriRef != null) {
            boolean valid = true;

            // Ensure that all characters are valid, otherwise encode them
            for (int i = 0; valid && (i < uriRef.length()); i++) {
                if (!isValid(uriRef.charAt(i))) {
                    valid = false;
                    Context.getCurrentLogger().fine(
                            "Invalid character detected in URI reference at index '"
                                    + i + "': \"" + uriRef.charAt(i)
                                    + "\". It will be automatically encoded.");
                } else if ((uriRef.charAt(i) == '%')
                        && (i > uriRef.length() - 2)) {
                    // A percent encoding character has been detected but
                    // without the necessary two hexadecimal digits following
                    valid = false;
                    Context.getCurrentLogger().fine(
                            "Invalid percent encoding detected in URI reference at index '"
                                    + i + "': \"" + uriRef.charAt(i)
                                    + "\". It will be automatically encoded.");
                }
            }

            if (!valid) {
                StringBuilder sb = new StringBuilder();

                for (int i = 0; (i < uriRef.length()); i++) {
                    if (isValid(uriRef.charAt(i))) {
                        if ((uriRef.charAt(i) == '%')
                                && (i > uriRef.length() - 2)) {
                            sb.append("%25");
                        } else {
                            sb.append(uriRef.charAt(i));
                        }
                    } else {
                        sb.append(encode(String.valueOf(uriRef.charAt(i))));
                    }
                }

                result = sb.toString();
            }
        }

        return result;
    }

    /**
     * Indicates whether some other object is "equal to" this one.
     * 
     * @param object
     *            The object to compare to.
     * @return True if this object is the same as the obj argument.
     */
    @Override
    public boolean equals(Object object) {
        if (object instanceof Reference) {
            final Reference ref = (Reference) object;
            if (this.internalRef == null) {
                return ref.internalRef == null;
            }
            return this.internalRef.equals(ref.internalRef);

        }

        return false;
    }

    /**
     * Returns the authority component for hierarchical identifiers. Includes
     * the user info, host name and the host port number.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The authority component for hierarchical identifiers.
     */
    public String getAuthority() {
        final String part = isRelative() ? getRelativePart()
                : getSchemeSpecificPart();

        if ((part != null) && part.startsWith("//")) {
            int index = part.indexOf('/', 2);

            if (index != -1) {
                return part.substring(2, index);
            }

            index = part.indexOf('?');
            if (index != -1) {
                return part.substring(2, index);
            }

            return part.substring(2);

        }

        return null;
    }

    /**
     * Returns the optionnally decoded authority component.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded authority component.
     * @see #getAuthority()
     */
    public String getAuthority(boolean decode) {
        return decode ? decode(getAuthority()) : getAuthority();
    }

    /**
     * Returns the base reference for relative references.
     * 
     * @return The base reference for relative references.
     */
    public Reference getBaseRef() {
        return this.baseRef;
    }

    /**
     * Returns the optional extensions for hierarchical identifiers. An
     * extensions part starts after the first '.' character of the last path
     * segment and ends with either the end of the segment of with the first ';'
     * character (matrix start). It is a token similar to file extensions
     * separated by '.' characters. The value can be ommited.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The extensions or null.
     * @see #getExtensionsAsArray()
     * @see #setExtensions(String)
     */
    public String getExtensions() {
        String result = null;
        final String lastSegment = getLastSegment();

        if (lastSegment != null) {
            final int extensionIndex = lastSegment.indexOf('.');
            final int matrixIndex = lastSegment.indexOf(';');

            if (extensionIndex != -1) {
                // Extensions found
                if (matrixIndex != -1) {
                    result = lastSegment.substring(extensionIndex + 1,
                            matrixIndex);
                } else {
                    // No matrix found
                    result = lastSegment.substring(extensionIndex + 1);
                }
            }
        }

        return result;
    }

    /**
     * Returns the extensions as an array or null if no extension is found.
     * 
     * @return The extensions as an array or null if no extension is found.
     * @see #getExtensions()
     */
    public String[] getExtensionsAsArray() {
        String[] result = null;
        final String extensions = getExtensions();

        if (extensions != null) {
            result = extensions.split("\\.");
        }

        return result;
    }

    /**
     * Returns the fragment identifier.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The fragment identifier.
     */
    public String getFragment() {
        if (hasFragment()) {
            return this.internalRef.substring(this.fragmentIndex + 1);
        }

        return null;
    }

    /**
     * Returns the optionnally decoded fragment identifier.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded fragment identifier.
     * @see #getFragment()
     */
    public String getFragment(boolean decode) {
        return decode ? decode(getFragment()) : getFragment();
    }

    /**
     * Returns the hierarchical part which is equivalent to the scheme specific
     * part less the query component.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The hierarchical part .
     */
    public String getHierarchicalPart() {
        if (hasScheme()) {
            // Scheme found
            if (hasQuery()) {
                // Query found
                return this.internalRef.substring(this.schemeIndex + 1,
                        this.queryIndex);
            }
            // No query found
            if (hasFragment()) {
                // Fragment found
                return this.internalRef.substring(this.schemeIndex + 1,
                        this.fragmentIndex);
            }
            // No fragment found
            return this.internalRef.substring(this.schemeIndex + 1);

        }

        // No scheme found
        if (hasQuery()) {
            // Query found
            return this.internalRef.substring(0, this.queryIndex);
        }
        if (hasFragment()) {
            // Fragment found
            return this.internalRef.substring(0, this.fragmentIndex);
        }

        // No fragment found
        return this.internalRef;
    }

    /**
     * Returns the optionnally decoded hierarchical part.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded hierarchical part.
     * @see #getHierarchicalPart()
     */
    public String getHierarchicalPart(boolean decode) {
        return decode ? decode(getHierarchicalPart()) : getHierarchicalPart();
    }

    /**
     * Returns the host domain name component for server based hierarchical
     * identifiers. It can also be replaced by an IP address when no domain name
     * was registered.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The host domain name component for server based hierarchical
     *         identifiers.
     */
    public String getHostDomain() {
        String result = null;
        final String authority = getAuthority();

        if (authority != null) {
            final int index1 = authority.indexOf('@');
            // We must prevent the case where the userinfo part contains ':'
            final int index2 = authority.indexOf(':', (index1 == -1 ? 0
                    : index1));

            if (index1 != -1) {
                // User info found
                if (index2 != -1) {
                    // Port found
                    result = authority.substring(index1 + 1, index2);
                } else {
                    // No port found
                    result = authority.substring(index1 + 1);
                }
            } else {
                // No user info found
                if (index2 != -1) {
                    // Port found
                    result = authority.substring(0, index2);
                } else {
                    // No port found
                    result = authority;
                }
            }
        }

        return result;
    }

    /**
     * Returns the optionnally decoded host domain name component.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded host domain name component.
     * @see #getHostDomain()
     */
    public String getHostDomain(boolean decode) {
        return decode ? decode(getHostDomain()) : getHostDomain();
    }

    /**
     * Returns the host identifier. Includes the scheme, the host name and the
     * host port number.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The host identifier.
     */
    public String getHostIdentifier() {
        final StringBuilder result = new StringBuilder();
        result.append(getScheme()).append("://").append(getAuthority());
        return result.toString();
    }

    /**
     * Returns the optionnally decoded host identifier.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded host identifier.
     * @see #getHostIdentifier()
     */
    public String getHostIdentifier(boolean decode) {
        return decode ? decode(getHostIdentifier()) : getHostIdentifier();
    }

    /**
     * Returns the optional port number for server based hierarchical
     * identifiers.
     * 
     * @return The optional port number for server based hierarchical
     *         identifiers or -1 if the port number does not exist.
     */
    public int getHostPort() {
        int result = -1;
        final String authority = getAuthority();

        if (authority != null) {
            final int index1 = authority.indexOf('@');
            // We must prevent the case where the userinfo part contains ':'
            final int index = authority.indexOf(':',
                    (index1 == -1 ? 0 : index1));

            if (index != -1) {
                try {
                    result = Integer.parseInt(authority.substring(index + 1));
                } catch (NumberFormatException nfe) {
                    Context.getCurrentLogger().log(
                            Level.WARNING,
                            "Can't parse hostPort : [hostRef,requestUri]=["
                                    + getBaseRef() + "," + this.internalRef
                                    + "]");
                }
            }
        }

        return result;
    }

    /**
     * Returns the absolute resource identifier, without the fragment.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The absolute resource identifier, without the fragment.
     */
    public String getIdentifier() {
        if (hasFragment()) {
            // Fragment found
            return this.internalRef.substring(0, this.fragmentIndex);
        }

        // No fragment found
        return this.internalRef;
    }

    /**
     * Returns the optionnally decoded absolute resource identifier.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded absolute resource identifier.
     * @see #getIdentifier()
     */
    public String getIdentifier(boolean decode) {
        return decode ? decode(getIdentifier()) : getIdentifier();
    }

    /**
     * Returns the last segment of a hierarchical path.<br>
     * For example the "/a/b/c" and "/a/b/c/" paths have the same segments: "a",
     * "b", "c.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The last segment of a hierarchical path.
     */
    public String getLastSegment() {
        String result = null;
        String path = getPath();

        if (path != null) {
            if (path.endsWith("/")) {
                path = path.substring(0, path.length() - 1);
            }

            final int lastSlash = path.lastIndexOf('/');

            if (lastSlash != -1) {
                result = path.substring(lastSlash + 1);
            }
        }

        return result;
    }

    /**
     * Returns the optionnally decoded last segment.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded last segment.
     * @see #getLastSegment()
     */
    public String getLastSegment(boolean decode) {
        return getLastSegment(decode, false);
    }

    /**
     * Returns the optionnally decoded last segment.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @param excludeMatrix
     * @return The optionnally decoded last segment.
     * @see #getLastSegment()
     */
    public String getLastSegment(boolean decode, boolean excludeMatrix) {
        String result = getLastSegment();

        if (excludeMatrix && (result != null)) {
            final int matrixIndex = result.indexOf(';');

            if (matrixIndex != -1) {
                result = result.substring(0, matrixIndex);
            }
        }

        return decode ? decode(result) : result;
    }

    /**
     * Returns the optional matrix for hierarchical identifiers. A matrix part
     * starts after the first ';' character of the last path segment. It is a
     * sequence of 'name=value' parameters separated by ';' characters. The
     * value can be ommited.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The matrix or null.
     */
    public String getMatrix() {
        final String lastSegment = getLastSegment();
        if (lastSegment != null) {
            final int matrixIndex = lastSegment.indexOf(';');

            if (matrixIndex != -1) {
                return lastSegment.substring(matrixIndex + 1);
            }
        }

        // No matrix found
        return null;
    }

    /**
     * Returns the optionnally decoded matrix.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded matrix.
     * @see #getMatrix()
     */
    public String getMatrix(boolean decode) {
        return decode ? decode(getMatrix()) : getMatrix();
    }

    /**
     * Returns the optional matrix as a form.
     * 
     * @return The optional matrix component as a form.
     */
    public Form getMatrixAsForm() {
        return new Form(getMatrix(), ';');
    }

    /**
     * Returns the optional matrix as a form submission.
     * 
     * @param characterSet
     *            The supported character encoding.
     * @return The optional matrix as a form.
     */
    public Form getMatrixAsForm(CharacterSet characterSet) {
        return new Form(getMatrix(), characterSet, ';');
    }

    /**
     * Returns the parent reference of a hierarchical reference. The last slash
     * of the path will be considered as the end of the parent path.
     * 
     * @return The parent reference of a hierarchical reference.
     */
    public Reference getParentRef() {
        Reference result = null;

        if (isHierarchical()) {
            String parentRef = null;
            String path = getPath();
            if (!path.equals("/") && !path.equals("")) {
                if (path.endsWith("/")) {
                    path = path.substring(0, path.length() - 1);
                }

                parentRef = getHostIdentifier()
                        + path.substring(0, path.lastIndexOf('/') + 1);
            } else {
                parentRef = this.internalRef;
            }

            result = new Reference(parentRef);
        }

        return result;
    }

    /**
     * Returns the path component for hierarchical identifiers. If not path is
     * available it returns null.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The path component for hierarchical identifiers.
     */
    public String getPath() {
        String result = null;
        final String part = isRelative() ? getRelativePart()
                : getSchemeSpecificPart();

        if (part != null) {
            if (part.startsWith("//")) {
                // Authority found
                final int index1 = part.indexOf('/', 2);

                if (index1 != -1) {
                    // Path found
                    final int index2 = part.indexOf('?');
                    if (index2 != -1) {
                        // Query found
                        result = part.substring(index1, index2);
                    } else {
                        // No query found
                        result = part.substring(index1);
                    }
                } else {
                    // Path must be empty in this case
                }
            } else {
                // No authority found
                final int index = part.indexOf('?');
                if (index != -1) {
                    // Query found
                    result = part.substring(0, index);
                } else {
                    // No query found
                    result = part;
                }
            }
        }

        return result;
    }

    /**
     * Returns the optionnally decoded path component. If not path is available
     * it returns null.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded path component.
     * @see #getPath()
     */
    public String getPath(boolean decode) {
        return decode ? decode(getPath()) : getPath();
    }

    /**
     * Returns the optional query component for hierarchical identifiers.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The query component or null.
     */
    public String getQuery() {
        if (hasQuery()) {
            // Query found
            if (hasFragment()) {
                if (this.queryIndex < this.fragmentIndex) {
                    // Fragment found and query sign not inside fragment
                    return this.internalRef.substring(this.queryIndex + 1,
                            this.fragmentIndex);
                }

                return null;
            }

            // No fragment found
            return this.internalRef.substring(this.queryIndex + 1);
        }

        // No query found
        return null;
    }

    /**
     * Returns the optionnally decoded query component.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded query component.
     * @see #getQuery()
     */
    public String getQuery(boolean decode) {
        return decode ? decode(getQuery()) : getQuery();
    }

    /**
     * Returns the optional query component as a form.
     * 
     * @return The optional query component as a form.
     */
    public Form getQueryAsForm() {
        return new Form(getQuery());
    }

    /**
     * Returns the optional query component as a form submission.
     * 
     * @param characterSet
     *            The supported character encoding.
     * @return The optional query component as a form submission.
     */
    public Form getQueryAsForm(CharacterSet characterSet) {
        return new Form(getQuery(), characterSet);
    }

    /**
     * Returns the relative part of relative references, without the query and
     * fragment. If the reference is absolute, then null is returned.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The relative part.
     */
    public String getRelativePart() {
        return isRelative() ? toString(false, false) : null;
    }

    /**
     * Returns the optionnally decoded relative part.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded relative part.
     * @see #getRelativePart()
     */
    public String getRelativePart(boolean decode) {
        return decode ? decode(getRelativePart()) : getRelativePart();
    }

    /**
     * Returns the current reference as a relative reference to the current base
     * reference. This method should only be invoked for absolute references,
     * otherwise an IllegalArgumentException will be raised.
     * 
     * @return The current reference as a relative reference to the current base
     *         reference.
     * @see #getRelativeRef(Reference)
     */
    public Reference getRelativeRef() {
        return getRelativeRef(getBaseRef());
    }

    /**
     * Returns the current reference relatively to a base reference. This method
     * should only be invoked for absolute references, otherwise an
     * IllegalArgumentException will be raised.
     * 
     * @param base
     *            The base reference to use.
     * @throws IllegalArgumentException
     *             If the relative reference is computed although the reference
     *             or the base reference are not absolute or not hierarchical.
     * @return The current reference relatively to a base reference.
     */
    public Reference getRelativeRef(Reference base) {
        Reference result = null;

        if (base == null) {
            result = this;
        } else if (!isAbsolute() || !isHierarchical()) {
            throw new IllegalArgumentException(
                    "The reference must have an absolute hierarchical path component");
        } else if (!base.isAbsolute() || !base.isHierarchical()) {
            throw new IllegalArgumentException(
                    "The base reference must have an absolute hierarchical path component");
        } else if (!getHostIdentifier().equals(base.getHostIdentifier())) {
            result = this;
        } else {
            final String localPath = getPath();
            final String basePath = base.getPath();
            String relativePath = null;

            if ((basePath == null) || (localPath == null)) {
                relativePath = localPath;
            } else {
                // Find the junction point
                boolean diffFound = false;
                int lastSlashIndex = -1;
                int i = 0;
                char current;
                while (!diffFound && (i < localPath.length())
                        && (i < basePath.length())) {
                    current = localPath.charAt(i);

                    if (current != basePath.charAt(i)) {
                        diffFound = true;
                    } else {
                        if (current == '/') {
                            lastSlashIndex = i;
                        }
                        i++;
                    }
                }

                if (!diffFound) {
                    if (localPath.length() == basePath.length()) {
                        // Both paths are strictly equivalent
                        relativePath = ".";
                    } else if (i == localPath.length()) {
                        // End of local path reached
                        if (basePath.charAt(i) == '/') {
                            if ((i + 1) == basePath.length()) {
                                // Both paths are strictly equivalent
                                relativePath = ".";
                            } else {
                                // The local path is a direct parent of the base
                                // path
                                // We need to add enough ".." in the relative
                                // path
                                final StringBuilder sb = new StringBuilder();

                                // Count segments
                                int segments = 0;
                                for (int j = basePath.indexOf('/', i); j != -1; j = basePath
                                        .indexOf('/', j + 1))
                                    segments++;

                                // Build relative path
                                for (int j = 0; j < segments; j++)
                                    sb.append("../");

                                int lastLocalSlash = localPath.lastIndexOf('/');
                                sb.append(localPath
                                        .substring(lastLocalSlash + 1));

                                relativePath = sb.toString();
                            }
                        } else {
                            // The base path has a segment that starts like
                            // the last local path segment
                            // But that is longer. Situation similar to a
                            // junction
                            final StringBuilder sb = new StringBuilder();

                            // Count segments
                            int segments = 0;
                            for (int j = basePath.indexOf('/', i); j != -1; j = basePath
                                    .indexOf('/', j + 1))
                                segments++;

                            // Build relative path
                            for (int j = 0; j < segments; j++)
                                if (j > 0)
                                    sb.append("/..");
                                else
                                    sb.append("..");

                            relativePath = sb.toString();

                            if (relativePath.equals("")) {
                                relativePath = ".";
                            }
                        }
                    } else if (i == basePath.length()) {
                        if (localPath.charAt(i) == '/') {
                            if ((i + 1) == localPath.length()) {
                                // Both paths are strictly equivalent
                                relativePath = ".";
                            } else {
                                // The local path is a direct child of the base
                                // path
                                relativePath = localPath.substring(i + 1);
                            }
                        } else {
                            if (lastSlashIndex == (i - 1)) {
                                // The local path is a direct subpath of the
                                // base path
                                relativePath = localPath.substring(i);
                            } else {
                                relativePath = ".."
                                        + localPath.substring(lastSlashIndex);
                            }
                        }
                    }
                } else {
                    // We found a junction point, we need to add enough ".." in
                    // the relative path and append the rest of the local path
                    // the local path is a direct subpath of the base path
                    final StringBuilder sb = new StringBuilder();

                    // Count segments
                    int segments = 0;
                    for (int j = basePath.indexOf('/', i); j != -1; j = basePath
                            .indexOf('/', j + 1))
                        segments++;

                    // Build relative path
                    for (int j = 0; j < segments; j++)
                        sb.append("../");

                    sb.append(localPath.substring(lastSlashIndex + 1));

                    relativePath = sb.toString();
                }
            }

            // Build the result reference
            result = new Reference();
            final String query = getQuery();
            final String fragment = getFragment();
            boolean modified = false;

            if ((query != null) && (!query.equals(base.getQuery()))) {
                result.setQuery(query);
                modified = true;
            }

            if ((fragment != null) && (!fragment.equals(base.getFragment()))) {
                result.setFragment(fragment);
                modified = true;
            }

            if (!modified || !relativePath.equals(".")) {
                result.setPath(relativePath);
            }
        }

        return result;
    }

    /**
     * Returns the part of the resource identifier remaining after the base
     * reference. Note that the optional fragment is not returned by this
     * method. Must be used with the following prerequisites:
     * <ul>
     * <li>the reference is absolute</li>
     * <li>the reference identifier starts with the resource baseRef</li>
     * </ul>
     * <br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The remaining resource part or null if the prerequisites are not
     *         satisfied.
     * @see #getRemainingPart(boolean)
     */
    public String getRemainingPart() {
        return getRemainingPart(false, true);
    }

    /**
     * Returns the optionally decoded remaining part.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionally decoded remaining part.
     * @see #getRemainingPart()
     */
    public String getRemainingPart(boolean decode) {
        return getRemainingPart(true, true);
    }

    /**
     * Returns the optionally decoded remaining part with or without the query
     * part of the reference.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @param query
     *            True if the query part should be returned, false otherwise.
     * @return The optionally decoded remaining part.
     * @see #getRemainingPart()
     */
    public String getRemainingPart(boolean decode, boolean query) {
        String result = null;
        final String all = toString(query, false);

        if (getBaseRef() != null) {
            final String base = getBaseRef().toString(query, false);

            if ((base != null) && all.startsWith(base)) {
                result = all.substring(base.length());
            }
        } else {
            result = all;
        }

        return decode ? decode(result) : result;
    }

    /**
     * Returns the scheme component.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The scheme component.
     */
    public String getScheme() {
        if (hasScheme()) {
            // Scheme found
            return this.internalRef.substring(0, this.schemeIndex);
        }

        // No scheme found
        return null;
    }

    /**
     * Returns the optionnally decoded scheme component.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded scheme component.
     * @see #getScheme()
     */
    public String getScheme(boolean decode) {
        return decode ? decode(getScheme()) : getScheme();
    }

    /**
     * Returns the protocol associated with the scheme component.
     * 
     * @return The protocol associated with the scheme component.
     */
    public Protocol getSchemeProtocol() {
        return Protocol.valueOf(getScheme());
    }

    /**
     * Returns the scheme specific part.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The scheme specific part.
     */
    public String getSchemeSpecificPart() {
        String result = null;

        if (hasScheme()) {
            // Scheme found
            if (hasFragment()) {
                // Fragment found
                result = this.internalRef.substring(this.schemeIndex + 1,
                        this.fragmentIndex);
            } else {
                // No fragment found
                result = this.internalRef.substring(this.schemeIndex + 1);
            }
        }

        return result;
    }

    /**
     * Returns the optionnally decoded scheme specific part.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded scheme specific part.
     * @see #getSchemeSpecificPart()
     */
    public String getSchemeSpecificPart(boolean decode) {
        return decode ? decode(getSchemeSpecificPart())
                : getSchemeSpecificPart();
    }

    /**
     * Returns the list of segments in a hierarchical path.<br>
     * A new list is created for each call.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The segments of a hierarchical path.
     */
    public List<String> getSegments() {
        final List<String> result = new ArrayList<String>();
        final String path = getPath();
        int start = -2; // The index of the slash starting the segment
        char current;

        if (path != null) {
            for (int i = 0; i < path.length(); i++) {
                current = path.charAt(i);

                if (current == '/') {
                    if (start == -2) {
                        // Beginning of an absolute path or sequence of two
                        // separators
                        start = i;
                    } else {
                        // End of a segment
                        result.add(path.substring(start + 1, i));
                        start = i;
                    }
                } else {
                    if (start == -2) {
                        // Starting a new segment for a relative path
                        start = -1;
                    } else {
                        // Looking for the next character
                    }
                }
            }

            if (start != -2) {
                // Add the last segment
                result.add(path.substring(start + 1));
            }
        }

        return result;
    }

    /**
     * Returns the optionnally decoded list of segments.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded list of segments.
     * @see #getSegments()
     */
    public List<String> getSegments(boolean decode) {
        final List<String> result = getSegments();

        if (decode) {
            for (int i = 0; i < result.size(); i++) {
                result.set(i, decode(result.get(i)));
            }
        }

        return result;
    }

    /**
     * Returns the target reference. This method resolves relative references
     * against the base reference then normalize them.
     * 
     * @throws IllegalArgumentException
     *             If the base reference (after resolution) is not absolute.
     * @throws IllegalArgumentException
     *             If the reference is relative and not base reference has been
     *             provided.
     * 
     * @return The target reference.
     */
    public Reference getTargetRef() {
        Reference result = null;

        // Step 1 - Resolve relative reference against their base reference
        if (isRelative() && (this.baseRef != null)) {
            Reference baseReference = null;

            if (this.baseRef.isAbsolute()) {
                baseReference = this.baseRef;
            } else {
                baseReference = this.baseRef.getTargetRef();
            }

            if (baseReference.isRelative()) {
                throw new IllegalArgumentException(
                        "The base reference must have an absolute hierarchical path component");
            }

            // Relative URI detected
            final String authority = getAuthority();
            final String path = getPath();
            final String query = getQuery();
            final String fragment = getFragment();

            // Create an empty reference
            result = new Reference();
            result.setScheme(baseReference.getScheme());

            if (authority != null) {
                result.setAuthority(authority);
                result.setPath(path);
                result.setQuery(query);
            } else {
                result.setAuthority(baseReference.getAuthority());

                if ((path == null) || (path.equals(""))) {
                    result.setPath(baseReference.getPath());

                    if (query != null) {
                        result.setQuery(query);
                    } else {
                        result.setQuery(baseReference.getQuery());
                    }
                } else {
                    if (path.startsWith("/")) {
                        result.setPath(path);
                    } else {
                        final String basePath = baseReference.getPath();
                        String mergedPath = null;

                        if ((baseReference.getAuthority() != null)
                                && ((basePath == null) || (basePath.equals("")))) {
                            mergedPath = "/" + path;
                        } else {
                            // Remove the last segment which may be empty if
                            // the path is ending with a slash
                            final int lastSlash = basePath.lastIndexOf('/');
                            if (lastSlash == -1) {
                                mergedPath = path;
                            } else {
                                mergedPath = basePath.substring(0,
                                        lastSlash + 1)
                                        + path;
                            }
                        }

                        result.setPath(mergedPath);
                    }

                    result.setQuery(query);
                }
            }

            result.setFragment(fragment);
        } else if (isRelative()) {
            // Relative reference with no baseRef detected
            throw new IllegalArgumentException(
                    "Relative references are only usable when a base reference is set.");
        } else {
            // Absolute URI detected
            result = new Reference(this.internalRef);
        }

        // Step 2 - Normalize the target reference
        result.normalize();

        return result;
    }

    /**
     * Returns the user info component for server based hierarchical
     * identifiers.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @return The user info component for server based hierarchical
     *         identifiers.
     */
    public String getUserInfo() {
        String result = null;
        final String authority = getAuthority();

        if (authority != null) {
            final int index = authority.indexOf('@');

            if (index != -1) {
                result = authority.substring(0, index);
            }
        }

        return result;
    }

    /**
     * Returns the optionnally decoded user info component.
     * 
     * @param decode
     *            Indicates if the result should be decoded using the
     *            {@link #decode(String)} method.
     * @return The optionnally decoded user info component.
     * @see #getUserInfo()
     */
    public String getUserInfo(boolean decode) {
        return decode ? decode(getUserInfo()) : getUserInfo();
    }

    /**
     * Indicates if this reference has file-like extensions on its last path
     * segment.
     * 
     * @return True if there is are extensions.
     * @see #getExtensions()
     */
    public boolean hasExtensions() {
        boolean result = false;

        // If these reference ends with a "/", it cannot be a file.
        final String path = getPath();
        if (!((path != null) && path.endsWith("/"))) {
            final String lastSegment = getLastSegment();

            if (lastSegment != null) {
                final int extensionsIndex = lastSegment.indexOf('.');
                final int matrixIndex = lastSegment.indexOf(';');
                result = (extensionsIndex != -1)
                        && ((matrixIndex == -1) || (extensionsIndex < matrixIndex));
            }
        }

        return result;
    }

    /**
     * Indicates if this reference has a fragment identifier.
     * 
     * @return True if there is a fragment identifier.
     */
    public boolean hasFragment() {
        return (this.fragmentIndex != -1);
    }

    /**
     * Returns a hash code value for the object.
     * 
     * @return A hash code value for the object.
     */
    @Override
    public int hashCode() {
        return (this.internalRef == null) ? 0 : this.internalRef.hashCode();
    }

    /**
     * Indicates if this reference has a matrix.
     * 
     * @return True if there is a matrix.
     * @see #getMatrix()
     */
    public boolean hasMatrix() {
        return (getLastSegment().indexOf(';') != -1);
    }

    /**
     * Indicates if this reference has a query component.
     * 
     * @return True if there is a query.
     */
    public boolean hasQuery() {
        return (this.queryIndex != -1);
    }

    /**
     * Indicates if this reference has a scheme component.
     * 
     * @return True if there is a scheme component.
     */
    public boolean hasScheme() {
        return (this.schemeIndex != -1);
    }

    /**
     * Indicates if the reference is absolute.
     * 
     * @return True if the reference is absolute.
     */
    public boolean isAbsolute() {
        return (getScheme() != null);
    }

    /**
     * Returns true if both reference are equivalent, meaning that they resolve
     * to the same target reference.
     * 
     * @param ref
     *            The reference to compare.
     * @return True if both reference are equivalent.
     */
    public boolean isEquivalentTo(Reference ref) {
        return getTargetRef().equals(ref.getTargetRef());
    }

    /**
     * Indicates if the identifier is hierarchical.
     * 
     * @return True if the identifier is hierarchical, false if it is opaque.
     */
    public boolean isHierarchical() {
        return isRelative() || (getSchemeSpecificPart().charAt(0) == '/');
    }

    /**
     * Indicates if the identifier is opaque.
     * 
     * @return True if the identifier is opaque, false if it is hierarchical.
     */
    public boolean isOpaque() {
        return isAbsolute() && (getSchemeSpecificPart().charAt(0) != '/');
    }

    /**
     * Indicates if the reference is a parent of the hierarchical child
     * reference.
     * 
     * @param childRef
     *            The hierarchical reference.
     * @return True if the reference is a parent of the hierarchical child
     *         reference.
     */
    public boolean isParent(Reference childRef) {
        boolean result = false;

        if ((childRef != null) && (childRef.isHierarchical())) {
            result = childRef.toString(false, false).startsWith(
                    toString(false, false));
        }

        return result;
    }

    /**
     * Indicates if the reference is relative.
     * 
     * @return True if the reference is relative.
     */
    public boolean isRelative() {
        return (getScheme() == null);
    }

    /**
     * Normalizes the reference. Useful before comparison between references or
     * when building a target reference from a base and a relative references.
     * 
     * @return The current reference.
     */
    public Reference normalize() {
        // 1. The input buffer is initialized with the now-appended path
        // components and the output buffer is initialized to the empty string.
        final StringBuilder output = new StringBuilder();
        final StringBuilder input = new StringBuilder();
        final String path = getPath();
        if (path != null) {
            input.append(path);
        }

        // 2. While the input buffer is not empty, loop as follows:
        while (input.length() > 0) {
            // A. If the input buffer begins with a prefix of "../" or "./",
            // then remove that prefix from the input buffer; otherwise,
            if ((input.length() >= 3) && input.substring(0, 3).equals("../")) {
                input.delete(0, 3);
            } else if ((input.length() >= 2)
                    && input.substring(0, 2).equals("./")) {
                input.delete(0, 2);
            }

            // B. if the input buffer begins with a prefix of "/./" or "/.",
            // where "." is a complete path segment, then replace that
            // prefix with "/" in the input buffer; otherwise,
            else if ((input.length() >= 3)
                    && input.substring(0, 3).equals("/./")) {
                input.delete(0, 2);
            } else if ((input.length() == 2)
                    && input.substring(0, 2).equals("/.")) {
                input.delete(1, 2);
            }

            // C. if the input buffer begins with a prefix of "/../" or "/..",
            // where ".." is a complete path segment, then replace that prefix
            // with "/" in the input buffer and remove the last segment and its
            // preceding "/" (if any) from the output buffer; otherwise,
            else if ((input.length() >= 4)
                    && input.substring(0, 4).equals("/../")) {
                input.delete(0, 3);
                removeLastSegment(output);
            } else if ((input.length() == 3)
                    && input.substring(0, 3).equals("/..")) {
                input.delete(1, 3);
                removeLastSegment(output);
            }

            // D. if the input buffer consists only of "." or "..", then remove
            // that from the input buffer; otherwise,
            else if ((input.length() == 1) && input.substring(0, 1).equals(".")) {
                input.delete(0, 1);
            } else if ((input.length() == 2)
                    && input.substring(0, 2).equals("..")) {
                input.delete(0, 2);
            }

            // E. move the first path segment in the input buffer to the end of
            // the output buffer, including the initial "/" character (if any)
            // and any subsequent characters up to, but not including, the next
            // "/" character or the end of the input buffer.
            else {
                int max = -1;
                for (int i = 1; (max == -1) && (i < input.length()); i++) {
                    if (input.charAt(i) == '/') {
                        max = i;
                    }
                }

                if (max != -1) {
                    // We found the next "/" character.
                    output.append(input.substring(0, max));
                    input.delete(0, max);
                } else {
                    // End of input buffer reached
                    output.append(input);
                    input.delete(0, input.length());
                }
            }
        }

        // Finally, the output buffer is returned as the result
        setPath(output.toString());

        // Ensure that the scheme and host names are reset in lower case
        setScheme(getScheme());
        setHostDomain(getHostDomain());

        // Remove the port if it is equal to the default port of the reference's
        // Protocol.
        final int hostPort = getHostPort();
        if (hostPort != -1) {
            final int defaultPort = Protocol.valueOf(getScheme())
                    .getDefaultPort();
            if (hostPort == defaultPort) {
                setHostPort(null);
            }
        }

        return this;
    }

    /**
     * Removes the last segement from the output builder.
     * 
     * @param output
     *            The output builder to update.
     */
    private void removeLastSegment(StringBuilder output) {
        int min = -1;
        for (int i = output.length() - 1; (min == -1) && (i >= 0); i--) {
            if (output.charAt(i) == '/') {
                min = i;
            }
        }

        if (min != -1) {
            // We found the previous "/" character.
            output.delete(min, output.length());
        } else {
            // End of output buffer reached
            output.delete(0, output.length());
        }

    }

    /**
     * Sets the authority component for hierarchical identifiers.
     * 
     * @param authority
     *            The authority component for hierarchical identifiers.
     */
    public void setAuthority(String authority) {
        final String oldPart = isRelative() ? getRelativePart()
                : getSchemeSpecificPart();
        String newPart;
        final String newAuthority = (authority == null) ? "" : "//" + authority;

        if (oldPart == null) {
            newPart = newAuthority;
        } else if (oldPart.startsWith("//")) {
            int index = oldPart.indexOf('/', 2);

            if (index != -1) {
                newPart = newAuthority + oldPart.substring(index);
            } else {
                index = oldPart.indexOf('?');
                if (index != -1) {
                    newPart = newAuthority + oldPart.substring(index);
                } else {
                    newPart = newAuthority;
                }
            }
        } else {
            newPart = newAuthority + oldPart;
        }

        if (isAbsolute()) {
            setSchemeSpecificPart(newPart);
        } else {
            setRelativePart(newPart);
        }
    }

    /**
     * Sets the base reference for relative references.
     * 
     * @param baseRef
     *            The base reference for relative references.
     */
    public void setBaseRef(Reference baseRef) {
        this.baseRef = baseRef;
    }

    /**
     * Sets the base reference for relative references.
     * 
     * @param baseUri
     *            The base URI for relative references.
     */
    public void setBaseRef(String baseUri) {
        setBaseRef(new Reference(baseUri));
    }

    /**
     * Sets the extensions for hierarchical identifiers. An extensions part
     * starts after the first '.' character of the last path segment and ends
     * with either the end of the segment of with the first ';' character
     * (matrix start). It is a token similar to file extensions separated by '.'
     * characters. The value can be ommited.<br>
     * Note that no URI decoding is done by this method.
     * 
     * @param extensions
     *            The extensions to set or null (without leading or trailing
     *            dots).
     * @see #getExtensions()
     * @see #getExtensionsAsArray()
     * @see #setExtensions(String[])
     */
    public void setExtensions(String extensions) {
        final String lastSegment = getLastSegment();

        if (lastSegment != null) {
            final int extensionIndex = lastSegment.indexOf('.');
            final int matrixIndex = lastSegment.indexOf(';');
            final StringBuilder sb = new StringBuilder();

            if (extensionIndex != -1) {
                // Extensions found
                sb.append(lastSegment.substring(0, extensionIndex));

                if ((extensions != null) && (extensions.length() > 0)) {
                    sb.append('.').append(extensions);
                }

                if (matrixIndex != -1) {
                    sb.append(lastSegment.substring(matrixIndex));
                }
            } else {
                // Extensions not found
                if ((extensions != null) && (extensions.length() > 0)) {
                    if (matrixIndex != -1) {
                        // Matrix found, make sure we append it
                        // after the extensions
                        sb.append(lastSegment.substring(0, matrixIndex))
                                .append('.').append(extensions).append(
                                        lastSegment.substring(matrixIndex));
                    } else {
                        // No matrix found, just append the extensions
                        sb.append(lastSegment).append('.').append(extensions);
                    }
                } else {
                    // No change necessary
                    sb.append(lastSegment);
                }
            }

            // Finally update the last segment
            setLastSegment(sb.toString());
        } else {
            setLastSegment('.' + extensions);
        }
    }

    /**
     * Sets the extensions based on an array of extension tokens (without dots).
     * 
     * @param extensions
     *            The array of extensions.
     * @see #getExtensions()
     * @see #getExtensionsAsArray()
     * @see #setExtensions(String)
     */
    public void setExtensions(String[] extensions) {
        String exts = null;

        if (extensions != null) {
            final StringBuilder sb = new StringBuilder();

            for (int i = 0; i < extensions.length; i++) {
                if (i > 0) {
                    sb.append('.');
                }

                sb.append(extensions[i]);
            }

            exts = sb.toString();
        }

        setExtensions(exts);
    }

    /**
     * Sets the fragment identifier.
     * 
     * @param fragment
     *            The fragment identifier.
     * @throws IllegalArgumentException
     *             if the fragment parameter contains the fragment delimiter
     *             ('#').
     */
    public void setFragment(String fragment) {
        fragment = encodeInvalidCharacters(fragment);

        if ((fragment != null) && (fragment.indexOf('#') != -1)) {
            throw new IllegalArgumentException(
                    "Illegal '#' character detected in parameter");
        }

        if (hasFragment()) {
            // Existing fragment
            if (fragment != null) {
                this.internalRef = this.internalRef.substring(0,
                        this.fragmentIndex + 1)
                        + fragment;
            } else {
                this.internalRef = this.internalRef.substring(0,
                        this.fragmentIndex);
            }
        } else {
            // No existing fragment
            if (fragment != null) {
                if (this.internalRef != null) {
                    this.internalRef = this.internalRef + '#' + fragment;
                } else {
                    this.internalRef = '#' + fragment;
                }
            } else {
                // Do nothing
            }
        }

        updateIndexes();
    }

    /**
     * Sets the host domain component for server based hierarchical identifiers.
     * 
     * @param domain
     *            The host component for server based hierarchical identifiers.
     */
    public void setHostDomain(String domain) {
        final String authority = getAuthority();

        if (authority == null) {
            setAuthority(domain);
        } else {
            if (domain == null) {
                domain = "";
            } else {
                // URI specification indicates that host names should be
                // produced in lower case
                domain = domain.toLowerCase();
            }

            final int index1 = authority.indexOf('@');
            // We must prevent the case where the userinfo part contains ':'
            final int index2 = authority.indexOf(':', (index1 == -1 ? 0
                    : index1));

            if (index1 != -1) {
                // User info found
                if (index2 != -1) {
                    // Port found
                    setAuthority(authority.substring(0, index1 + 1) + domain
                            + authority.substring(index2));
                } else {
                    // No port found
                    setAuthority(authority.substring(0, index1 + 1) + domain);
                }
            } else {
                // No user info found
                if (index2 != -1) {
                    // Port found
                    setAuthority(domain + authority.substring(index2));
                } else {
                    // No port found
                    setAuthority(domain);
                }
            }
        }
    }

    /**
     * Sets the optional port number for server based hierarchical identifiers.
     * 
     * @param port
     *            The optional port number for server based hierarchical
     *            identifiers.
     * @throws IllegalArgumentException
     *             If the autority has not been defined.
     */
    public void setHostPort(Integer port) {
        final String authority = getAuthority();

        if (authority != null) {
            final int index1 = authority.indexOf('@');
            // We must prevent the case where the userinfo part contains ':'
            final int index = authority.indexOf(':',
                    (index1 == -1 ? 0 : index1));
            final String newPort = (port == null) ? "" : ":" + port;

            if (index != -1) {
                setAuthority(authority.substring(0, index) + newPort);
            } else {
                setAuthority(authority + newPort);
            }
        } else {
            throw new IllegalArgumentException(
                    "No authority defined, please define a host name first");
        }
    }

    /**
     * Sets the absolute resource identifier.
     * 
     * @param identifier
     *            The absolute resource identifier.
     * @throws IllegalArgumentException
     *             If the identifier parameter contains the fragment delimiter
     *             ('#').
     */
    public void setIdentifier(String identifier) {
        identifier = encodeInvalidCharacters(identifier);

        if (identifier == null) {
            identifier = "";
        }

        if (identifier.indexOf('#') != -1) {
            throw new IllegalArgumentException(
                    "Illegal '#' character detected in parameter");
        }

        if (hasFragment()) {
            // Fragment found
            this.internalRef = identifier
                    + this.internalRef.substring(this.fragmentIndex);
        } else {
            // No fragment found
            this.internalRef = identifier;
        }

        updateIndexes();
    }

    /**
     * Sets the last segment of the path. If no path is available, then it
     * creates one and adds a slash in front of the given last segmetn. <br>
     * Note that no URI decoding is done by this method.
     * 
     * @param lastSegment
     *            The last segment of a hierarchical path.
     */
    public void setLastSegment(String lastSegment) {
        final String path = getPath();
        final int lastSlashIndex = path.lastIndexOf('/');

        if (lastSlashIndex != -1) {
            setPath(path.substring(0, lastSlashIndex + 1) + lastSegment);
        } else {
            setPath('/' + lastSegment);
        }
    }

    /**
     * Sets the path component for hierarchical identifiers.
     * 
     * @param path
     *            The path component for hierarchical identifiers.
     */
    public void setPath(String path) {
        final String oldPart = isRelative() ? getRelativePart()
                : getSchemeSpecificPart();
        String newPart = null;

        if (oldPart != null) {
            if (path == null) {
                path = "";
            }

            if (oldPart.startsWith("//")) {
                // Authority found
                final int index1 = oldPart.indexOf('/', 2);

                if (index1 != -1) {
                    // Path found
                    final int index2 = oldPart.indexOf('?');

                    if (index2 != -1) {
                        // Query found
                        newPart = oldPart.substring(0, index1) + path
                                + oldPart.substring(index2);
                    } else {
                        // No query found
                        newPart = oldPart.substring(0, index1) + path;
                    }
                } else {
                    // No path found
                    final int index2 = oldPart.indexOf('?');

                    if (index2 != -1) {
                        // Query found
                        newPart = oldPart.substring(0, index2) + path
                                + oldPart.substring(index2);
                    } else {
                        // No query found
                        newPart = oldPart + path;
                    }
                }
            } else {
                // No authority found
                final int index = oldPart.indexOf('?');

                if (index != -1) {
                    // Query found
                    newPart = path + oldPart.substring(index);
                } else {
                    // No query found
                    newPart = path;
                }
            }
        } else {
            newPart = path;
        }

        if (isAbsolute()) {
            setSchemeSpecificPart(newPart);
        } else {
            setRelativePart(newPart);
        }
    }

    /**
     * Sets the scheme component based on this protocol.
     * 
     * @param protocol
     *            The protocol of the scheme component.
     */
    public void setProtocol(Protocol protocol) {
        setScheme(protocol.getSchemeName());
    }

    /**
     * Sets the query component for hierarchical identifiers.
     * 
     * @param query
     *            The query component for hierarchical identifiers.
     */
    public void setQuery(String query) {
        query = encodeInvalidCharacters(query);
        final boolean emptyQueryString = ((query == null) || (query.length() <= 0));

        if (hasQuery()) {
            // Query found
            if (hasFragment()) {
                // Fragment found
                if (!emptyQueryString) {
                    this.internalRef = this.internalRef.substring(0,
                            this.queryIndex + 1)
                            + query
                            + this.internalRef.substring(this.fragmentIndex);
                } else {
                    this.internalRef = this.internalRef.substring(0,
                            this.queryIndex)
                            + this.internalRef.substring(this.fragmentIndex);
                }
            } else {
                // No fragment found
                if (!emptyQueryString) {
                    this.internalRef = this.internalRef.substring(0,
                            this.queryIndex + 1)
                            + query;
                } else {
                    this.internalRef = this.internalRef.substring(0,
                            this.queryIndex);
                }
            }
        } else {
            // No query found
            if (hasFragment()) {
                // Fragment found
                if (!emptyQueryString) {
                    this.internalRef = this.internalRef.substring(0,
                            this.fragmentIndex)
                            + '?'
                            + query
                            + this.internalRef.substring(this.fragmentIndex);
                } else {
                    // Do nothing;
                }
            } else {
                // No fragment found
                if (!emptyQueryString) {
                    if (this.internalRef != null) {
                        this.internalRef = this.internalRef + '?' + query;
                    } else {
                        this.internalRef = '?' + query;
                    }
                } else {
                    // Do nothing;
                }
            }
        }

        updateIndexes();
    }

    /**
     * Sets the relative part for relative references only.
     * 
     * @param relativePart
     *            The relative part to set.
     */
    public void setRelativePart(String relativePart) {
        relativePart = encodeInvalidCharacters(relativePart);

        if (relativePart == null) {
            relativePart = "";
        }

        if (!hasScheme()) {
            // This is a relative reference, no scheme found
            if (hasQuery()) {
                // Query found
                this.internalRef = relativePart
                        + this.internalRef.substring(this.queryIndex);
            } else if (hasFragment()) {
                // Fragment found
                this.internalRef = relativePart
                        + this.internalRef.substring(this.fragmentIndex);
            } else {
                // No fragment found
                this.internalRef = relativePart;
            }
        }

        updateIndexes();
    }

    /**
     * Sets the scheme component.
     * 
     * @param scheme
     *            The scheme component.
     */
    public void setScheme(String scheme) {
        scheme = encodeInvalidCharacters(scheme);

        if (scheme != null) {
            // URI specification indicates that scheme names should be
            // produced in lower case
            scheme = scheme.toLowerCase();
        }

        if (hasScheme()) {
            // Scheme found
            if (scheme != null) {
                this.internalRef = scheme
                        + this.internalRef.substring(this.schemeIndex);
            } else {
                this.internalRef = this.internalRef
                        .substring(this.schemeIndex + 1);
            }
        } else {
            // No scheme found
            if (scheme != null) {
                if (this.internalRef == null) {
                    this.internalRef = scheme + ':';
                } else {
                    this.internalRef = scheme + ':' + this.internalRef;
                }
            }
        }

        updateIndexes();
    }

    /**
     * Sets the scheme specific part.
     * 
     * @param schemeSpecificPart
     *            The scheme specific part.
     */
    public void setSchemeSpecificPart(String schemeSpecificPart) {
        schemeSpecificPart = encodeInvalidCharacters(schemeSpecificPart);

        if (schemeSpecificPart == null) {
            schemeSpecificPart = "";
        }

        if (hasScheme()) {
            // Scheme found
            if (hasFragment()) {
                // Fragment found
                this.internalRef = this.internalRef.substring(0,
                        this.schemeIndex + 1)
                        + schemeSpecificPart
                        + this.internalRef.substring(this.fragmentIndex);
            } else {
                // No fragment found
                this.internalRef = this.internalRef.substring(0,
                        this.schemeIndex + 1)
                        + schemeSpecificPart;
            }
        } else {
            // No scheme found
            if (hasFragment()) {
                // Fragment found
                this.internalRef = schemeSpecificPart
                        + this.internalRef.substring(this.fragmentIndex);
            } else {
                // No fragment found
                this.internalRef = schemeSpecificPart;
            }
        }

        updateIndexes();
    }

    /**
     * Sets the segments of a hierarchical path.<br>
     * A new absolute path will replace any existing one.
     * 
     * @param segments
     *            The segments of the hierarchical path.
     */
    public void setSegments(List<String> segments) {
        final StringBuilder sb = new StringBuilder();

        for (final String segment : segments) {
            sb.append('/').append(segment);
        }

        setPath(sb.toString());
    }

    /**
     * Sets the user info component for server based hierarchical identifiers.
     * 
     * @param userInfo
     *            The user info component for server based hierarchical
     *            identifiers.
     * @throws IllegalArgumentException
     *             If the autority part has not been defined.
     */
    public void setUserInfo(String userInfo) {
        final String authority = getAuthority();

        if (authority != null) {
            final int index = authority.indexOf('@');
            final String newUserInfo = (userInfo == null) ? "" : userInfo + '@';

            if (index != -1) {
                setAuthority(newUserInfo + authority.substring(index + 1));
            } else {
                setAuthority(newUserInfo + authority);
            }
        } else {
            throw new IllegalArgumentException(
                    "No authority defined, please define a host name first");
        }
    }

    /**
     * Returns the reference as an URI string.
     * 
     * @return The reference as an URI string.
     */
    @Override
    public String toString() {
        return this.internalRef;
    }

    /**
     * Returns the URI reference string.
     * 
     * @param query
     *            Indicates if the query should be included;
     * @param fragment
     *            Indicates if the fragment should be included;
     * @return The URI reference string.
     */
    public String toString(boolean query, boolean fragment) {
        if (query) {
            if (fragment) {
                return this.internalRef;
            }

            if (hasFragment()) {
                return this.internalRef.substring(0, this.fragmentIndex);
            }
            return this.internalRef;
        }

        if (fragment) {
            // Fragment should be included
            if (hasQuery()) {
                // Query found
                if (hasFragment()) {
                    // Fragment found
                    return this.internalRef.substring(0, this.queryIndex) + "#"
                            + getFragment();
                }

                // No fragment found
                return this.internalRef.substring(0, this.queryIndex);
            }

            // No query found
            return this.internalRef;
        }

        // Fragment should not be included
        if (hasQuery()) {
            // Query found
            return this.internalRef.substring(0, this.queryIndex);
        }
        if (hasFragment()) {
            // Fragment found
            return this.internalRef.substring(0, this.fragmentIndex);
        }

        return this.internalRef;
    }

    /**
     * Converts to a {@link java.net.URI} instance. Note that relative
     * references are resolved before conversion using the
     * {@link #getTargetRef()} method.
     * 
     * @return A {@link java.net.URI} instance.
     */
    public java.net.URI toUri() {
        return java.net.URI.create(getTargetRef().toString());
    }

    /**
     * Converts to a {@link java.net.URL} instance. Note that relative
     * references are resolved before conversion using the
     * {@link #getTargetRef()} method.
     * 
     * @return A {@link java.net.URL} instance.
     */
    public java.net.URL toUrl() {
        java.net.URL result = null;

        try {
            result = new java.net.URL(getTargetRef().toString());
        } catch (java.net.MalformedURLException e) {
            throw new IllegalArgumentException("Malformed URL exception", e);
        }

        return result;
    }

    /**
     * Updates internal indexes.
     */
    private void updateIndexes() {
        if (this.internalRef != null) {
            // Compute the indexes
            final int firstSlashIndex = this.internalRef.indexOf('/');
            this.schemeIndex = this.internalRef.indexOf(':');

            if ((firstSlashIndex != -1) && (this.schemeIndex > firstSlashIndex)) {
                // We are in the rare case of a relative reference where one of
                // the path segments contains a colon character. In this case,
                // we ignore the colon as a valid scheme index.
                // Note that this colon can't be in the first segment as it is
                // forbidden by the URI RFC.
                this.schemeIndex = -1;
            }

            this.queryIndex = this.internalRef.indexOf('?');
            this.fragmentIndex = this.internalRef.indexOf('#');
            if (hasQuery() && hasFragment()
                    && (this.queryIndex > this.fragmentIndex)) {
                // Query sign inside fragment
                this.queryIndex = -1;
            }
        } else {
            this.schemeIndex = -1;
            this.queryIndex = -1;
            this.fragmentIndex = -1;
        }
    }
}
